/*
 * Copyright 2011 Philippe Blanc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package me.l1k3.fx.client.engine.impl;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.regexp.shared.SplitResult;

import me.l1k3.core.client.Core;
import me.l1k3.fx.client.channel.impl.StylesList;
import me.l1k3.fx.client.channel.inter.Channel;
import me.l1k3.fx.client.channel.inter.ChannelCSS3;
import me.l1k3.fx.client.engine.easing.Easing;
import me.l1k3.fx.client.engine.inter.FXEngineCSS3;
import me.l1k3.fx.client.engine.inter.FXEngineConnector;

public abstract class FXEngineImplAbstractCSS3 implements FXEngineCSS3 {
    private FXEngineConnector fx;
    private String easing;  
    private int delay;
    private int duration;
    private boolean running;
    private StylesList styles;
    private JavaScriptObject handle;
    private JavaScriptObject handleFire;
    private Scheduler.ScheduledCommand display;
    private Scheduler.ScheduledCommand fire;
    private Scheduler.ScheduledCommand start;
    
    @Override
    public final void setFX(FXEngineConnector fx) {
        this.fx = fx;
    }
    
    @Override
    public final void unsetFX(FXEngineConnector fx) {
        if(this.fx==fx) {
            if(handle!=null) {
                unRegisterTransitionEndEvent(fx.getContext().getElement(), getEventSignature(), handle);
                handle = null;
            }
            
            styles = null;
            handleFire = null;
            this.fx = null;
        }
    }
    
    @Override
    public final FXEngineConnector getFX() {
        return fx;
    }

    @Override
    public final void setEasing(String easing) throws Easing.EasingNotSupportedException {
        if(easing==null) return;
        
        easing = easing.toLowerCase();
        
        if(easing.equals(EASE)) {
            setEasing(0.25, 0.1, 0.25, 1.0);
        }
        else
        if(easing.equals(LINEAR)) {
            easing = "linear";
        }
        else
        if(easing.equals(EASE_IN)) {
            setEasing(0.42, 0, 1.0, 1.0);
        }
        else
        if(easing.equals(EASE_OUT)) {
            setEasing(0, 0, 0.58, 1.0);
        }
        else
        if(easing.equals(EASE_IN_OUT)) {
            setEasing(0.42, 0, 0.58, 1.0);
        }
        else {
            RegExp exp = RegExp.compile(REGEX_CUBIC_BEZIER);
            SplitResult result = exp.split(easing);
            
            if(result.length()!=4)
            throw new Easing.EasingNotSupportedException();
                
            setEasing(Double.valueOf(result.get(0)), Double.valueOf(result.get(1)), Double.valueOf(result.get(2)), Double.valueOf(result.get(3)));
        }
    }
    
    @Override
    public final void setEasing(Easing easing) throws Easing.EasingNotSupportedException {
        this.easing = easing.getCSS3Easing();
    }    
    
    @Override
    public final void setEasing(double x1, double y1, double x2, double y2) {
        easing = "cubic-bezier("+x1+", "+y1+", "+x2+", "+y2+")";
    }
    
    @Override
    public final String getEasing() {
        return easing;
    }  

    @Override
    public boolean validateChannel(Channel channel) {        
        if(channel instanceof ChannelCSS3) {
            //lazy creation
            if(styles==null) {
                styles = StylesList.create();
            }
            
            ((ChannelCSS3)channel).setCSS3Style(styles);
            
            return true;
        }
        
        return false;
    }
    
    @Override
    public final void cancel() {
        if (running) {
            onComplete();
        }
    }

    @Override
    public final void run(int duration) {
        this.delay = 0;
        this.duration = duration;
        run();
    }
    
    @Override
    public final void run(int duration, int delay) {
        this.delay = delay;
        this.duration = duration;
        run();
    }
    
    @Override
    public final void hookFire(int duration) {
        if(styles==null || styles.length()==0) {
            return;
        }
        
        if(handle==null) {
            handle = createHandle(this, fx.getContext().getElement());
        }

        this.duration = duration;
        getFire().execute();
    }
    
    //- - -

    public final void run() {
        if(styles==null || styles.length()==0) {
            return;
        }
        
        if(duration<=0) {
            cancel();
            onStart(); 
            //no need to call fx.onUpdate(1.0) this should not be necessary 
            //for CSS3 implementation and hybridEngine will take care of 
            //any js implementation
            onComplete();
            return;
        }
        
        if(handle==null) {
            handle = createHandle(this, fx.getContext().getElement());
        }
        
        cancel();
        
        if(delay>0) {
            Core.createTimeout(getHandleFire(), delay);
        }
        else {
            fire();
        }
    }

    protected final void onStart() {
        running = true;
        fx.onStart();
    }
    
    protected final void onComplete() {
        if(running) {
            running = false;
            removeTransition(fx.getContext().getStyle());
            unRegisterTransitionEndEvent(fx.getContext().getElement(), getEventSignature(), handle);
            fx.onComplete();
        }
    }
    
    protected final String getStyles() {
        if(styles!=null && styles.length()>0)
        return styles.styles();
        
        return null;
    }
    
    protected abstract void fire(Style style, String styles, String easing, String duration);
    protected abstract String getEventSignature();
    protected abstract void removeTransition(Style style);
    
    protected final void fire() {
        if(fx.getContext().isShowAndHide()) {
            Scheduler.get().scheduleDeferred(getDisplay());
        }
        else {
            Scheduler.get().scheduleDeferred(getFire());
        }
    }
    
    protected final Scheduler.ScheduledCommand getDisplay() {
        if(display==null) display = new Scheduler.ScheduledCommand() {
            @Override
            public void execute() {
                fx.getContext().setVisible(true);
                Scheduler.get().scheduleDeferred(getFire());
            }
        };
        
        return display;
    }
    
    protected final Scheduler.ScheduledCommand getFire() {
        if(fire==null) fire = new Scheduler.ScheduledCommand() {
            @Override
            public void execute() {
                registerTransitionEndEvent(fx.getContext().getElement(), getEventSignature(), handle);
                fire(fx.getContext().getStyle(), styles.styles(), easing==null? "cubic-bezier(0.25, 0.1, 0.25, 1.0)" : easing, duration+"ms");
                Scheduler.get().scheduleDeferred(getStart());
            }
        };
        
        return fire;
    }
    
    protected final Scheduler.ScheduledCommand getStart() {
        if(start==null) start = new Scheduler.ScheduledCommand() {
            @Override
            public void execute() {
                onStart();
            }
        };
        
        return start;
    }
    
    protected final JavaScriptObject getHandleFire() {
        if(handleFire==null) handleFire = createFireHandle(this);
        
        return handleFire;
    }
    
    private final static native void registerTransitionEndEvent(Element element, String eventName, JavaScriptObject handle) /*-{
        element.addEventListener(eventName, handle, false);
    }-*/;

    private final static native void unRegisterTransitionEndEvent(Element element, String eventName, JavaScriptObject handle) /*-{
        element.removeEventListener(eventName, handle, false);
    }-*/;
    
    private final static native JavaScriptObject createHandle(FXEngineImplAbstractCSS3 engine, Element element) /*-{
        return function(e) {if(e.target==element) $entry(engine.@me.l1k3.fx.client.engine.impl.FXEngineImplCSS3::onComplete()()); e.stopPropagation();};
    }-*/;
    
    private final static native JavaScriptObject createFireHandle(FXEngineImplAbstractCSS3 engine) /*-{  
        return function() {$entry(engine.@me.l1k3.fx.client.engine.impl.FXEngineImplCSS3::fire()());};
    }-*/;
    
    //- - -
    
    @Override
    public final String toString() {
        return CSS3;
    }
}
